<?php

namespace NetworkIOEvent;

use Timer\Timer;
use Exception;

class StaticServer
{
    private static int $autoincrement_id = 0;
    private static bool $notify_if_have_data = false;

    private static array $host_name_ips_cache = [];
    private static int $host_name_ips_cache_time = 2 * 60 * 60; // 2h

    private static array $id_resource_map = []; // 所有的ID和它们对应的流资源（stream_socket_client、stream_socket_accept和stream_socket_server返回的）
    private static array $id_read_buffer = []; // 暂存从ID对应的流读取的数据（含未能被业务逻辑处理完成的残余数据），Key是ID，值是一个字符串
    private static array $id_write_buffer = []; // 暂存需要通过ID对应的流发送出去的数据（含未能被stream_send_to发送完成的残余数据），Key是ID，值是一个字符串
    private static array $client_id_resource_map = []; // ID和它们对应的流资源（包含stream_socket_client、stream_socket_accept返回的）
    private static array $server_id_resource_map = []; // ID和它们对应的流资源（只包含stream_socket_server返回的）
    private static array $id_will_close_connection = []; // 需要在创建连接函数触发、读取事件函数触发、写入事件函数触发，全部触发后再关闭的流资源的ID。Key是ID，值是对应的流资源
    private static array $id_new_connection_callback = []; // 需要触发handleOpenEvent的回调的资源对应ID和它们的流资源。
    private static array $id_read_activity_in_select = []; // 是否在select时发生可读的事件

    protected static function handleReadEvent(int $id, string &$readBuffer): int
    {
        return strlen($readBuffer);
    }

    protected static function handleWriteEvent(int $id, string &$writeBuffer)
    {
    }

    protected static function handleOpenEvent(int $id)
    {
    }

    protected static function handleCloseEvent(int $id)
    {
    }

    protected final static function createConnection(string $host, int $port): int
    {
        if (static::$autoincrement_id < 0) {
            static::$autoincrement_id = 0;
        }
        do {
            $id = ++static::$autoincrement_id;
        } while (isset(static::$id_resource_map[$id]));
        // Check whether it is an IP address. If it is not, you need to resolve the domain name.
        $mayIpAddressLong = ip2long($host);
        if ($mayIpAddressLong === false || $mayIpAddressLong === -1) { // not a ip
            $now = time();
            if (
                isset(static::$host_name_ips_cache[$host])
                && $now - static::$host_name_ips_cache[$host]['time'] < static::$host_name_ips_cache_time
            ) {
                $cache = static::$host_name_ips_cache[$host];
                $ipsList = $cache['ips'];
                $key = mt_rand(0, $cache['total'] - 1);
                $ip = $ipsList[$key];
            } else {
                if (empty($ipsList = gethostbynamel($host))) {
                    throw new Exception("Failed to connect the remote server. Host: $host");
                }
                $cache = ['ips' => $ipsList, 'total' => count($ipsList, COUNT_NORMAL), 'time' => $now];
                $key = mt_rand(0, $cache['total'] - 1);
                $ip = $ipsList[$key];
                static::$host_name_ips_cache[$host] = $cache;
            }
            $serverInfo = "tcp://$ip:$port";
        } else {
            $serverInfo = "tcp://$host:$port";
        }
        $client = @stream_socket_client($serverInfo, $errorCode, $errorMessage, 0, STREAM_CLIENT_ASYNC_CONNECT);
        if (false === $client) {
            throw new Exception("Failed to connect the remote server. Error code: $errorCode, error message: $errorMessage. Server info: $serverInfo");
        }
        if (false === stream_set_blocking($client, false)) {
            throw new Exception('Call stream_set_blocking() fail');
        }
        if (false === stream_set_read_buffer($client, 0)) {
            throw new Exception('Call stream_set_read_buffer() fail');
        }
        if (function_exists('socket_import_stream')) {
            if (false !== ($s = socket_import_stream($client))) {
                if (false === socket_set_option($s, SOL_SOCKET, SO_LINGER, ['l_onoff' => 1, 'l_linger' => 0])) {
                    throw new Exception('Set SO_LINGER option fail');
                }
                if (false === socket_set_option($s, SOL_SOCKET, SO_KEEPALIVE, 1)) {
                    throw new Exception('Set SO_KEEPALIVE option fail');
                }
            } else {
                throw new Exception('Call socket_import_stream() fail');
            }
        }
        static::$id_resource_map[$id] = $client;
        static::$id_read_buffer[$id] = '';
        static::$id_write_buffer[$id] = '';
        static::$client_id_resource_map[$id] = $client;
        static::$id_new_connection_callback[$id] = $client;
        return $id;
    }

    protected final static function closeConnection(int $id)
    {
        if (isset(static::$client_id_resource_map[$id])) {
            static::$id_will_close_connection[$id] = static::$client_id_resource_map[$id];
        }
    }

    protected final static function debugGetResourceNumber(): int
    {
        return count(static::$id_resource_map, COUNT_NORMAL);
    }

    public static function setNotifyIfHaveData(bool $val)
    {
        static::$notify_if_have_data = $val;
    }

    public static function setHostNameIpsCacheTime(int $time)
    {
        static::$host_name_ips_cache_time = $time;
    }

    public static function getHostNameIpCache(): array
    {
        return static::$host_name_ips_cache;
    }

    public static function setHostNameIpCache(string $host, array $ips, ?int $cacheTime = null)
    {
        static::$host_name_ips_cache[$host] = [
            'ips' => $ips,
            'total' => count($ips, COUNT_NORMAL),
            'time' => is_null($cacheTime) ? static::$host_name_ips_cache_time : $cacheTime,
        ];
    }

    public static function deleteHostNameIpCache(string $host)
    {
        if (isset(static::$host_name_ips_cache[$host])) {
            unset(static::$host_name_ips_cache[$host]);
        }
    }

    public static function run(string $ip, int $port)
    {
        if (false === ($server = @stream_socket_server("tcp://$ip:$port", $error_code, $error_message, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, stream_context_create(['socket' => ['tcp_nodelay' => true]])))) {
            throw new Exception("创建服务器失败，错误代码：{$error_code}，错误信息：{$error_message}。");
        }
        if (false === stream_set_blocking($server, false)) {
            throw new Exception('Call stream_set_blocking() fail');
        }
        if (false === stream_set_read_buffer($server, 0)) {
            throw new Exception('Call stream_set_read_buffer() fail');
        }
        if (false === stream_set_write_buffer($server, 0)) {
            throw new Exception('Call stream_set_write_buffer() fail');
        }
        if (function_exists('socket_import_stream')) {
            if (false !== ($s = socket_import_stream($server))) {
                if (false === socket_set_option($s, SOL_SOCKET, SO_LINGER, ['l_onoff' => 1, 'l_linger' => 0])) {
                    throw new Exception('Set SO_LINGER option fail');
                }
                if (false === socket_set_option($s, SOL_SOCKET, SO_KEEPALIVE, 1)) {
                    throw new Exception('Set SO_KEEPALIVE option fail');
                }
            } else {
                throw new Exception('Call socket_import_stream() fail');
            }
        }

        $id = ++static::$autoincrement_id;
        static::$id_resource_map[$id] = $server;
        static::$server_id_resource_map[$id] = $server;

        try {
            while (true) {
                $begin_time = new Timer();
                $data_read = 0; // 从所有的流资源读取的字节数
                $data_write = 0; // 从所有的流资源写入的字节数
                $event_handle_times = 0; // 所有的流资源通过stream_select触发的事件数
                $except = $read = static::$id_resource_map;
                $write = static::$client_id_resource_map;
                if (empty($write)) {
                    $write = null;
                }
                static::$id_read_activity_in_select = [];
                if (false === ($activity_stream_number = stream_select($read, $write, $except, 0, 200000))) {
                    throw new Exception('Call stream_select() fail');
                }
                foreach ($read as $id => $stream) {
                    if (isset(static::$server_id_resource_map[$id])) {
                        if (false !== ($client = @stream_socket_accept($server, 0))) {
                            if (false === stream_set_blocking($client, false)) {
                                throw new Exception('Call stream_set_blocking() fail');
                            }
                            if (false === stream_set_read_buffer($client, 0)) {
                                throw new Exception('Call stream_set_read_buffer() fail');
                            }
                            if (function_exists('socket_import_stream')) {
                                if (false !== ($s = socket_import_stream($client))) {
                                    if (false === socket_set_option($s, SOL_SOCKET, SO_LINGER, ['l_onoff' => 1, 'l_linger' => 0])) {
                                        throw new Exception('Set SO_LINGER option fail');
                                    }
                                    if (false === socket_set_option($s, SOL_SOCKET, SO_KEEPALIVE, 1)) {
                                        throw new Exception('Set SO_KEEPALIVE option fail');
                                    }
                                } else {
                                    throw new Exception('Call socket_import_stream() fail');
                                }
                            }
                            if (static::$autoincrement_id < 0) {
                                static::$autoincrement_id = 0;
                            }
                            do {
                                $id = ++static::$autoincrement_id;
                            } while (isset(static::$id_resource_map[$id]));
                            static::$id_resource_map[$id] = $client;
                            static::$id_read_buffer[$id] = '';
                            static::$id_write_buffer[$id] = '';
                            static::$client_id_resource_map[$id] = $client;
                            static::handleOpenEvent($id);
                            $event_handle_times++;
                        }
                    } else {
                        if (!is_resource($stream) || feof($stream)) {
                            if (isset(static::$id_new_connection_callback[$id])) {
                                unset(static::$id_new_connection_callback[$id]);
                                static::handleOpenEvent($id);
                            }
                            if (is_resource($stream)) {
                                stream_socket_shutdown($stream, STREAM_SHUT_RDWR);
                                $event_handle_times++;
                            }
                            unset(static::$id_resource_map[$id]);
                            unset(static::$id_read_buffer[$id]);
                            unset(static::$id_write_buffer[$id]);
                            unset(static::$client_id_resource_map[$id]);
                            static::handleCloseEvent($id);
                        } else {
                            if (false !== ($read_buffer = stream_get_contents($stream))) {
                                $event_handle_times++;
                                if ('' !== $read_buffer) {
                                    static::$id_read_buffer[$id] .= $read_buffer;
                                    // do read event
                                    if (isset(static::$id_new_connection_callback[$id])) {
                                        unset(static::$id_new_connection_callback[$id]);
                                        static::handleOpenEvent($id);
                                    }
                                    static::$id_read_activity_in_select[$id] = true;
                                    $read_size = static::handleReadEvent($id, static::$id_read_buffer[$id]);
                                    $data_read += $read_size;
                                    if ($read_size >= strlen(static::$id_read_buffer[$id])) {
                                        static::$id_read_buffer[$id] = '';
                                    } elseif ($read_size > 0) {
                                        static::$id_read_buffer[$id] = substr(static::$id_read_buffer[$id], $read_size);
                                    }
                                }
                            }
                            unset($read_buffer);
                        }
                    }
                }
                if (static::$notify_if_have_data) {
                    foreach (static::$id_read_buffer as $id => &$read_buffer) {
                        if ('' !== $read_buffer && !isset(static::$id_read_activity_in_select[$id])) {
                            // do read event
                            if (isset(static::$id_new_connection_callback[$id])) {
                                unset(static::$id_new_connection_callback[$id]);
                                static::handleOpenEvent($id);
                            }
                            $read_size = static::handleReadEvent($id, static::$id_read_buffer[$id]);
                            if ($read_size >= strlen(static::$id_read_buffer[$id])) {
                                static::$id_read_buffer[$id] = '';
                            } elseif ($read_size > 0) {
                                static::$id_read_buffer[$id] = substr(static::$id_read_buffer[$id], $read_size);
                            }
                        }
                    }
                    unset($read_buffer);
                }
                if (!is_null($write)) {
                    foreach ($write as $id => $stream) {
                        if (isset(static::$id_resource_map[$id])) {
                            if (!is_resource($stream) || feof($stream)) {
                                if (isset(static::$id_new_connection_callback[$id])) {
                                    unset(static::$id_new_connection_callback[$id]);
                                    if (isset(static::$id_resource_map[$id])) {
                                        static::handleOpenEvent($id);
                                    }
                                }
                                if (is_resource($stream)) {
                                    stream_socket_shutdown($stream, STREAM_SHUT_RDWR);
                                    $event_handle_times++;
                                }
                                if (isset(static::$id_resource_map[$id])) {
                                    static::handleCloseEvent($id);
                                }
                                if (isset(static::$id_resource_map[$id])) unset(static::$id_resource_map[$id]);
                                if (isset(static::$id_read_buffer[$id])) unset(static::$id_read_buffer[$id]);
                                if (isset(static::$id_write_buffer[$id])) unset(static::$id_write_buffer[$id]);
                                if (isset(static::$client_id_resource_map[$id])) unset(static::$client_id_resource_map[$id]);
                            } else {
                                if ('' === static::$id_write_buffer[$id]) {
                                    if (isset(static::$id_new_connection_callback[$id])) {
                                        unset(static::$id_new_connection_callback[$id]);
                                        static::handleOpenEvent($id);
                                    }
                                    static::handleWriteEvent($id, static::$id_write_buffer[$id]);
                                } else {
                                    $writeMaxSize = strlen(static::$id_write_buffer[$id]);
                                    $bufferSize = 4096;
                                    if ($writeMaxSize > $bufferSize) {
                                        $writeData = substr(static::$id_write_buffer[$id], 0, $bufferSize);
                                    } else {
                                        $writeData = static::$id_write_buffer[$id];
                                    }
                                    if (false !== ($write_size = @stream_socket_sendto($stream, $writeData))) {
                                        $event_handle_times++;
                                        $data_write += $write_size;
                                        if ($write_size >= $writeMaxSize) {
                                            static::$id_write_buffer[$id] = '';
                                        } elseif ($write_size > 0) {
                                            static::$id_write_buffer[$id] = substr(static::$id_write_buffer[$id], $write_size);
                                        }
                                    }
                                    unset($writeData);
                                }
                            }
                        }
                    }
                }
                foreach ($except as $id => $stream) {
                    if (isset(static::$id_new_connection_callback[$id])) {
                        unset(static::$id_new_connection_callback[$id]);
                        if (isset(static::$id_resource_map[$id])) {
                            static::handleOpenEvent($id);
                        }
                    }
                    if (is_resource($stream)) {
                        stream_socket_shutdown($stream, STREAM_SHUT_RDWR);
                        $event_handle_times++;
                    }
                    if (isset(static::$id_resource_map[$id])) {
                        static::handleCloseEvent($id);
                    }
                    if (isset(static::$id_resource_map[$id])) unset(static::$id_resource_map[$id]);
                    if (isset(static::$id_read_buffer[$id])) unset(static::$id_read_buffer[$id]);
                    if (isset(static::$id_write_buffer[$id])) unset(static::$id_write_buffer[$id]);
                    if (isset(static::$client_id_resource_map[$id])) unset(static::$client_id_resource_map[$id]);
                }
                foreach (static::$id_new_connection_callback as $id => $stream) {
                    if (isset(static::$id_resource_map[$id])) {
                        static::handleOpenEvent($id);
                    }
                }
                static::$id_new_connection_callback = [];
                foreach (static::$id_will_close_connection as $id => $stream) {
                    if (isset(static::$id_resource_map[$id])) {
                        if (is_resource($stream)) {
                            stream_socket_shutdown($stream, STREAM_SHUT_RDWR);
                        }
                        static::handleCloseEvent($id);
                        if (isset(static::$id_resource_map[$id])) unset(static::$id_resource_map[$id]);
                        if (isset(static::$id_read_buffer[$id])) unset(static::$id_read_buffer[$id]);
                        if (isset(static::$id_write_buffer[$id])) unset(static::$id_write_buffer[$id]);
                        if (isset(static::$client_id_resource_map[$id])) unset(static::$client_id_resource_map[$id]);
                    }
                }
                static::$id_will_close_connection = [];
                $end_time = new Timer();
                //echo "$event_handle_times, $data_read, $data_write" . PHP_EOL;
                //echo ' time: ' . $end_time->sub($begin_time);
                if (0 == $event_handle_times && 0 == $data_read && 0 == $data_write && $end_time->sub($begin_time) < 0.001) {
                    usleep(1000);
                }
                //echo PHP_EOL;
            }
        } catch (Exception $e) {
            foreach (static::$id_resource_map as $stream) {
                if (is_resource($stream)) {
                    stream_socket_shutdown($stream, STREAM_SHUT_RDWR);
                }
            }
            static::$id_resource_map = [];
            static::$id_read_buffer = [];
            static::$id_write_buffer = [];
            static::$client_id_resource_map = [];
            static::$server_id_resource_map = [];
            static::$id_will_close_connection = [];
            static::$id_new_connection_callback = [];
            throw $e;
        }
    }
}
